package felix.rule.service.actors;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import felix.actor.core.TActorRef;
import felix.actor.core.TActorSystem;
import felix.actor.core.msg.TActorMsg;
import felix.rule.api.RuleChainTransactionService;
import felix.rule.data.msg.EngineMsg;
import felix.rule.js.script.JsExecutorServiceImpl;
import felix.rule.js.script.JsInvokeService;
import felix.rule.service.ActorService;
import felix.rule.service.ComponentDiscoveryService;
import felix.rule.service.RuleChainService;
import lombok.Data;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;

import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

/**
 * 系统上下文环境
 *
 * @author Felix
 * @date 2020/8/24
 */
@Data
@Component
@Slf4j
public class ActorSystemContext {

    protected final ObjectMapper mapper = new ObjectMapper();

    private ActorService actorService;

    private TActorSystem actorSystem;

    private TActorRef appActor;

    @Autowired
    private ApplicationContext springContext;

    @Autowired
    private RuleChainService ruleChainService;

    @Autowired
    private ComponentDiscoveryService componentService;

    @Autowired(required = false)
    @Getter
    private RuleChainTransactionService ruleChainTransactionService;

    @Value("${actors.rule.chain.error_persist_frequency:3000}")
    private long ruleChainErrorPersistFrequency;
    @Value("${actors.rule.node.error_persist_frequency:3000}")
    private long ruleNodeErrorPersistFrequency;

    @Autowired
    private JsInvokeService jsSandbox;

    @Autowired
    private JsExecutorServiceImpl jsExecutor;

    public void tell(TActorMsg tActorMsg) {
        appActor.tell(tActorMsg);
    }

    public ScheduledExecutorService getScheduler() {
        return actorSystem.getScheduler();
    }

    public void schedulePeriodicMsgWithDelay(TActorRef ctx, TActorMsg msg, long delayInMs, long periodInMs) {
        log.debug("Scheduling periodic msg {} every {} ms with delay {} ms", msg, periodInMs, delayInMs);
        getScheduler().scheduleWithFixedDelay(() -> ctx.tell(msg), delayInMs, periodInMs, TimeUnit.MILLISECONDS);
    }

    public void scheduleMsgWithDelay(TActorRef ctx, TActorMsg msg, long delayInMs) {
        log.debug("Scheduling msg {} with delay {} ms", msg, delayInMs);
        getScheduler().schedule(() -> ctx.tell(msg), delayInMs, TimeUnit.MILLISECONDS);
    }

    public void persistDebugInput(String ruleId, String entityId, EngineMsg tbMsg, String relationType) {
        persistDebugAsync(ruleId, entityId, "IN", tbMsg, relationType, null);
    }

    public void persistDebugInput(String ruleId, String entityId, EngineMsg tbMsg, String relationType, Throwable error) {
        persistDebugAsync(ruleId, entityId, "IN", tbMsg, relationType, error);
    }

    public void persistDebugOutput(String ruleId, String entityId, EngineMsg tbMsg, String relationType, Throwable error) {
        persistDebugAsync(ruleId, entityId, "OUT", tbMsg, relationType, error);
    }

    public void persistDebugOutput(String ruleId, String entityId, EngineMsg tbMsg, String relationType) {
        persistDebugAsync(ruleId, entityId, "OUT", tbMsg, relationType, null);
    }

    private void persistDebugAsync(String ruleId, String entityId, String type, EngineMsg tbMsg, String relationType, Throwable error) {
        try {
            String metadata = mapper.writeValueAsString(tbMsg.getMetaData().getData());

            ObjectNode node = mapper.createObjectNode()
                    .put("ruleId", ruleId)
                    .put("entityId", entityId)
                    .put("type", type)
                    .put("originator", tbMsg.getOriginator())
                    .put("msgId", tbMsg.getId().toString())
                    .put("msgType", tbMsg.getType())
                    .put("dataType", tbMsg.getDataType().name())
                    .put("relationType", relationType)
                    .put("data", tbMsg.getData())
                    .put("metadata", metadata);

            if (error != null) {
                node = node.put("error", toString(error));
            }
            // TODO 异步日志存储
            log.info("persistDebugMsg: {}", node);
        } catch (IOException ex) {
            log.warn("Failed to persist rule node debug message", ex);
        }
    }

    private String toString(Throwable e) {
        StringWriter sw = new StringWriter();
        e.printStackTrace(new PrintWriter(sw));
        return sw.toString();
    }

}
